package org.vectrics.common.db;

import java.io.Serializable;
import java.sql.Timestamp;
import java.util.Iterator;

import org.apache.log4j.Logger;
import org.hibernate.CallbackException;
import org.hibernate.EntityMode;
import org.hibernate.Interceptor;
import org.hibernate.Transaction;
import org.hibernate.type.Type;
import org.vectrics.SystemException;
import org.vectrics.domain.Domain;
import org.vectrics.hibernate.ThreadProperties;
import org.vectrics.provider.CoreServiceLocator;
import org.vectrics.user.Person;

public class AuditInterceptor implements Interceptor, Serializable {
    private transient Logger log = Logger.getLogger(AuditInterceptor.class);

    public static final long serialVersionUID = 5245163477335635L;

    public static final String PROPERTY_LAST_MODIFY_PERSON = "lastModifiedBy";

    public static final String PROPERTY_LAST_MODIFY_DATE = "lastModifiedDate";

    public static final String PROPERTY_CREATED_PERSON = "createdBy";

    public static final String PROPERTY_CREATED_DATE = "createdDate";

    private boolean validateOwnedObjectOnLoad = false;

    private EventGenerator eventGenerator = null;

    public AuditInterceptor(EventGenerator eventGenerator) {
        this.eventGenerator = eventGenerator;
    }

    public String onPrepareStatement(String statement) {
        return (statement);
    }

    public void onCollectionUpdate(Object object, Serializable ser) {
        if (log.isDebugEnabled())
            log.debug("onCollectionUpdate() object: " + object + "  Serializable: " + ser);
    }

    public void onCollectionRemove(Object object, Serializable ser) {
        if (log.isDebugEnabled())
            log.debug("onCollectionRemove() object: " + object + "  Serializable: " + ser);
    }

    public void onCollectionRecreate(Object object, Serializable ser) {
        if (log.isDebugEnabled())
            log.debug("onCollectionRecreate() object: " + object + "  Serializable: " + ser);
    }

    public int[] findDirty(Object obj, Serializable ser, Object[] obs, Object[] ob2s, String[] strs, Type[] types) {
        // log.debug("findDirty() called");
        return (null);
    }

    public Object instantiate(String className, EntityMode entityMode, Serializable objectKey) throws CallbackException {
        // log.debug("instantiate() called");
        return (null);
    }

    public void beforeTransactionCompletion(Transaction transaction) {
        log.debug("beforeTransactionCompletion() called");
    }

    public void afterTransactionCompletion(Transaction transaction) {
        log.debug("afterTransactionCompletion() called");
    }

    public void afterTransactionBegin(Transaction transaction) {
        log.debug("afterTransactionBegin() called");
    }

    public Boolean isTransient(Object obj) {
        // log.debug("isTransient called");
        return (null);
    }

    public String getEntityName(Object obj) {
        // log.debug("getEntityName() called");
        return (null);
    }

    public Object getEntity(String name, Serializable s) {
        return (null);
    }

    /**
     * Hibernate interceptor method called when Hibernate is asked to load an
     * object from the database.
     */
    public boolean onLoad(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) {
        if (ThreadProperties.getSystemUser())
            return false;

        if ((entity instanceof OwnedObject) && (validateOwnedObjectOnLoad)) {
            if (entity instanceof Person) {
                log.debug("Ignoring domain authentication for Person object");
            } else {
                for (int i = 0; i < propertyNames.length; i++) {
                    String propertyName = propertyNames[i];
                    if (propertyName.equals("domain")) {
                        String domainValue = state[i].toString();
                        int posSep = domainValue.indexOf("|");
                        String domainIdStr = domainValue.substring(0, posSep);
                        if (log.isDebugEnabled()) {
                            log.debug("Class: " + entity.getClass().getName() + "  Number of properties: "
                                    + propertyNames.length);
                            log.debug("On Load, domainId = " + domainIdStr + "  State=" + state[i]);
                        }
                        Long domainId = new Long(domainIdStr);
                        Domain domain = CoreServiceLocator.getSystemService().findDomain(domainId);
                        if (domain == null)
                            throw new SystemException("Could not find data owner for id=" + domainIdStr);
                        if (domain.getVoided())
                            throw new SystemException("Data owner is void for id=" + domainIdStr);

                        // if (this.validateOwnedObjectOnLoad) {
                        // Person authPerson = ThreadProperties.getPerson();
                        // if (authPerson == null)
                        // throw new
                        // SystemException("Could not find authorized person");
                        // boolean canAccess =
                        // authPerson.hasDomainOrGlobalRole(UserPrivilegeType
                        // .GLOBAL_DATAOWNER_ACCESS,
                        // domain);
                        // if (canAccess == false)
                        // throw new NotAuthorizedException(
                        // "User not authorized to view object "
                        // + "\r\n      - Viewed Class:          " +
                        // entity.getClass().getName()
                        // + "\r\n      - Viewed Oject.toString: " +
                        // entity.toString()
                        // + "\r\n      - User:                  " +
                        // ThreadProperties.getPerson().getName()
                        // + "\r\n      - Obj Data Owner :       " +
                        // domain.getDescription());
                        // }
                    }
                }
            }
        }
        return false;
    }

    public void recordModifiedProperty(int propertyIndex, Object[] previousState, Object[] currentState,
            String[] propertyNames, ChangedPropertiesSet changedProperties) {

        String currentStateStr = "Not Assigned";
        if (currentState[propertyIndex] != null && currentState[propertyIndex].toString() != null)
            currentStateStr = currentState[propertyIndex].toString();

        String previousStateStr = "Not Assigned";
        if (previousState != null) {
            if (previousState[propertyIndex] != null)
                previousStateStr = previousState[propertyIndex].toString();
        }

        if (false == currentStateStr.equals(previousStateStr)) {
            changedProperties.add(new ChangedProperty(propertyNames[propertyIndex], previousStateStr, currentStateStr));
        } else {
            log.debug("\r\n\r\nProperty the same: " + propertyNames[propertyIndex] + " -   New Value: "
                    + currentStateStr
            // + "\r\n  Old Value: " + previousStateStr
                    );
        }
    }

    /**
     * 
     */
    public boolean onFlushDirty(Object entity, Serializable id, Object[] currentState, Object[] previousState,
            String[] propertyNames, Type[] types) {

        log.debug("onFlushDirty() called - reporting that some entity had changed properties");

        // if (this.eventGenerator != null) {
        // this.eventGenerator.handleCreatedEntity(entity);
        // }

        if (entity instanceof EventGeneratingEntity) {
            Auditable auditable = null;
            if (entity instanceof Auditable)
                auditable = (Auditable) entity;

            ChangeReportable reportable = null;
            if (entity instanceof ChangeReportable)
                reportable = (ChangeReportable) entity;

            log.debug("onFlushDirty() called - setting audit fields" + "\r\n - class type = "
                    + entity.getClass().getName());
            try {
                boolean updatedPerson = false;
                boolean updatedDate = false;
                boolean changed = false;

                ChangedPropertiesSet changedProperties = new ChangedPropertiesSet();

                Timestamp timestamp = new Timestamp(System.currentTimeMillis());
                for (int i = 0; i < propertyNames.length; i++) {
                    String propertyName = propertyNames[i];
                    if (log.isDebugEnabled())
                        log.debug("Looking for changes in property: " + propertyNames[i]);

                    if ((propertyName.equals("lastModifiedDate") || (propertyName.equals("lastModifiedBy")))) {
                        log.debug("Do not report changes in last modified date or person");
                    } else {
                        recordModifiedProperty(i, previousState, currentState, propertyNames, changedProperties);
                    }
                }
                changed = (changedProperties.size() > 0);

                if (changed) {
                    for (int i = 0; i < propertyNames.length; i++) {
                        String propertyName = propertyNames[i];
                        if (propertyName.equals(PROPERTY_LAST_MODIFY_PERSON)) {
                            currentState[i] = ThreadProperties.getPerson();
                            if (ThreadProperties.getSystemUser()) {
                                currentState[i] = CoreServiceLocator.getConfiguration().getSystemPerson();
                            }
                            updatedPerson = true;

                        } else if (propertyName.equals(PROPERTY_LAST_MODIFY_DATE)) {
                            currentState[i] = timestamp;
                            updatedDate = true;
                        }
                    }
                } else {
                    log.debug("Object did not change values");
                }

                if ((changed) && ((auditable != null) || (reportable != null))) {
                    this.reportChangedFields(entity, changedProperties);
                }

                if (changed) {
                    this.eventGenerator.handleModifiedEntity(entity, changedProperties);
                }

                if ((changed) && (auditable != null) && ((updatedPerson == false) || (updatedDate == false)))
                    throw new SystemException("Record changed, but failed to update person or date");

            } catch (Exception e) {
                log.error("Exception saving on updating fields", e);
            }

        } else {
            log.debug("onFlushDirty() called - no action for class type" + "\r\n - class type = "
                    + entity.getClass().getName());
        }

        return false;
    }

    private void reportChangedFields(Object entity, ChangedPropertiesSet changedProperties) {
        StringBuffer changeMessage = new StringBuffer();
        changeMessage.append("\r\nObject change: ");
        changeMessage.append(entity.getClass().getName());
        changeMessage.append("\r\nUser: " + ThreadProperties.getPerson());

        if (changedProperties != null) {
            Iterator iter = changedProperties.iterator();
            while (iter.hasNext()) {
                ChangedProperty changedProperty = (ChangedProperty) iter.next();
                changeMessage.append("\r\n => " + changedProperty.getName() + " From: " + changedProperty.getOldValue()
                        + " To: " + changedProperty.getNewValue());
            }
        }
        log.debug(changeMessage.toString());
    }

    public boolean onSave(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) {

        boolean saved = true;

        if (this.eventGenerator != null) {
            this.eventGenerator.handleCreatedEntity(entity);
        }

        if (entity instanceof Auditable) {
            log.debug("onSave() called - setting audit fields" + "\r\n - class type = " + entity.getClass().getName()
                    + "\r\n - class type = " + entity.toString());
            try {
                if (ThreadProperties.getPerson() == null)
                    throw new SystemException("Session user is null");
                boolean createdPerson = false;
                boolean createdDate = false;
                boolean updatedPerson = false;
                boolean updatedDate = false;
                Timestamp timestamp = new Timestamp(System.currentTimeMillis());
                for (int i = 0; i < propertyNames.length; i++) {
                    String propertyName = propertyNames[i];
                    if (propertyName.equals(PROPERTY_CREATED_PERSON)) {
                        state[i] = ThreadProperties.getPerson();
                        createdPerson = true;
                    } else if (propertyName.equals(PROPERTY_CREATED_DATE)) {
                        state[i] = timestamp;
                        createdDate = true;
                    } else if (propertyName.equals(PROPERTY_LAST_MODIFY_PERSON)) {
                        state[i] = ThreadProperties.getPerson();
                        updatedPerson = true;
                    } else if (propertyName.equals(PROPERTY_LAST_MODIFY_DATE)) {
                        state[i] = timestamp;
                        updatedDate = true;
                    }
                }
                if (updatedPerson == false)
                    throw new SystemException("Record added, but failed to set last mod person");
                if (updatedDate == false)
                    throw new SystemException("Record added, but failed to set last mod date");
                if (createdPerson == false)
                    throw new SystemException("Record added, but failed to set created person");
                if (createdDate == false)
                    throw new SystemException("Record added, but failed to set created date");
                saved = true;
            } catch (Exception e) {
                saved = false;
                log.error("Exception for auditable - create", e);
                throw new SystemException("Exception for auditable - create", e);
            }
            if (ThreadProperties.getPerson() == null)
                throw new SystemException("Transaction person not stored in for thread");
        } else {
            log
                    .debug("onSave() called - ignored for class type" + "\r\n - class type = "
                            + entity.getClass().getName());

        }
        return saved;
    }

    public void onDelete(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) {
        log.debug("onDelete() called");
    }

    public void preFlush(Iterator entities) {
    }

    public void postFlush(Iterator entities) {
        log.debug("postFlush() called");
    }

}
